<?php

namespace common\base;

use Yii;
use yii\helpers\ArrayHelper;
use yii\helpers\StringHelper;

/**
 * 扩展Yii2 自带加密解密处理
 *
 * @package common\base
 * @author emhome<emhome@163.com>
 * @since 2.0
 */
class Security extends \yii\base\Security {

    /**
     * @var string|null The cipher to use for encryption and decryption.
     */
    public $public_key;

    /**
     * @var string|null
     */
    public $private_key;

    /**
     * @var string|null
     */
    public $secret_key;

    /**
     * @var string|null
     */
    public $secret_password = '--password--';

    /**
     * @var string|null
     */
    public $simpleList = [29, 26, 14, 16, 15, 7, 8, 24, 30, 19, 5, 20, 31, 1, 17, 13, 0, 28, 2, 18, 27, 6, 21, 23, 10, 11, 22, 9, 12, 3, 25, 4];

    public function generateRandomKey($length = 32) {
        if ($length <= 0) {
            return '';
        }
        return parent::generateRandomKey($length);
    }

    /**
     * 简洁加密
     *
     * @param mixed $data 待加密数据
     * @return string base64_encode字符串
     */
    public function encryptSimple($data) {
        $source = base64_encode($data);
        $sourceArray = str_split($source, 32);
        $_data = '';
        foreach ($sourceArray as $chunk) {
            $_temp = str_split($chunk);
            $vbitpos = rand(0, 31);
            $vbit = '';
            $_chunk = '';
            foreach ($this->simpleList as $pos) {
                $c = isset($_temp[$pos]) ? $_temp[$pos] : ';';
                if ($pos == $vbitpos) {
                    $vbit = $c;
                }
                $_chunk .= $c;
            }
            $_data .= chr($vbitpos + 48) . $vbit . $_chunk;
        }
        return base64_encode($_data);
    }

    /**
     * 简洁解密
     *
     * @param string $data 待解密数据
     * @return mixed
     */
    public function decryptSimple($data) {
        $encryptData = base64_decode($data);
        $dist = str_split($encryptData, 34);
        $s = '';
        foreach ($dist as $chars) {
            $vbitpos = ord(substr($chars, 0, 1)) - 48;
            $vbit = substr($chars, 1, 1);
            $_temp = str_split(substr($chars, 2));
            $x = array_combine($this->simpleList, $_temp);
            ksort($x);
            if ($vbit == $x[$vbitpos]) {
                $s .= trim(implode($x), ';');
            }
        }
        return base64_decode($s);
    }

    /**
     * Encrypts data.
     *
     * @param string $data data to be encrypted
     * @param bool $passwordBased set true to use password-based key derivation
     * @param string $secret the encryption password or key
     * @param string|null $info context/application specific information, e.g. a user ID
     * See [RFC 5869 Section 3.2](https://tools.ietf.org/html/rfc5869#section-3.2) for more details.
     *
     * @return string the encrypted data
     * @throws InvalidConfigException on OpenSSL not loaded
     * @throws Exception on OpenSSL error
     * @see decrypt()
     */
    public function encryptNoDf($data, $key) {
        if (!extension_loaded('openssl')) {
            throw new InvalidConfigException('Encryption requires the OpenSSL PHP extension');
        }
        if (!isset($this->allowedCiphers[$this->cipher][0], $this->allowedCiphers[$this->cipher][1])) {
            throw new InvalidConfigException($this->cipher . ' is not an allowed cipher');
        }

        list($blockSize, $keySize) = $this->allowedCiphers[$this->cipher];

        $iv = $this->generateRandomKey($blockSize);

        $encrypted = openssl_encrypt($data, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
        if ($encrypted === false) {
            throw new \yii\base\Exception('OpenSSL failure on encryption: ' . openssl_error_string());
        }
        return $encrypted;
    }

    /**
     * Decrypts data.
     *
     * @param string $data encrypted data to be decrypted.
     * @param bool $passwordBased set true to use password-based key derivation
     * @param string $secret the decryption password or key
     * @param string|null $info context/application specific information, @see encrypt()
     *
     * @return bool|string the decrypted data or false on authentication failure
     * @throws InvalidConfigException on OpenSSL not loaded
     * @throws Exception on OpenSSL error
     * @see encrypt()
     */
    public function decryptNoDf($data, $key) {
        if (!extension_loaded('openssl')) {
            throw new InvalidConfigException('Encryption requires the OpenSSL PHP extension');
        }
        if (!isset($this->allowedCiphers[$this->cipher][0], $this->allowedCiphers[$this->cipher][1])) {
            throw new InvalidConfigException($this->cipher . ' is not an allowed cipher');
        }

        $decrypted = openssl_decrypt($data, $this->cipher, $key, OPENSSL_RAW_DATA, '');
        if ($decrypted === false) {
            throw new \yii\base\Exception('OpenSSL failure on decryption: ' . openssl_error_string());
        }
        return $decrypted;
    }

    /**
     * 加密
     *
     * @param mixed $data 待加密数据
     * @param string|bool $key 密钥
     * @return string base64_encode字符串
     */
    public function encryptMaster($data, $key = false) {
        if ($key === false) {
            $key = $this->generatePrivateKey($key);
        }
        $encryptByKey = $this->encryptByKey($data, $key, $this->public_key);
        return base64_encode($encryptByKey);
    }

    /**
     * 解密
     *
     * @param string $data 待解密数据
     * @param string|bool $key 密钥
     * @return mixed
     */
    public function decryptMaster($data, $key = false) {
        if ($key === false) {
            $key = $this->generatePrivateKey($key);
        }
        return $this->decryptByKey(base64_decode($data), $key, $this->public_key);
    }

    /**
     * 自定义加密
     *
     * @param string $data 待解密数据
     * @return string
     */
    public function generatePrivateKey($data = false) {
        if ($data === false) {
            $data = $this->private_key ?: md5($this->public_key);
        }
        $data_md5 = md5($data);
        $figure = preg_replace('/\D/s', '', $data_md5);
        $mlen = str_split($figure);
        $pos = array_sum($mlen) % 32 - 1;
        $string = '';
        foreach (str_split($data_md5) as $key => $char) {
            $string .= $char;
            if ($key == $pos) {
                $string .= $this->public_key;
            }
        }
        return md5($string);
    }

    /**
     * APP数据安全加密
     *
     * @param string $data 待加密数据
     * @param string $key 密钥
     * @return mixed
     */
    public function encryptDataByKey($data, $key = null) {
        if ($key === null) {
            $key = $this->secret_key;
        }
        list($blockSize, $keySize) = $this->allowedCiphers[$this->cipher];
        if (strlen($key) !== $keySize) {
            return false;
        }
        $pos = rand(0, $keySize);
        $iv = substr($key . $key, $pos, $blockSize);
        $prefix = chr($pos + 48);
        $encrypt_data = openssl_encrypt($data, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
        return base64_encode($prefix . base64_encode($encrypt_data));
    }

    /**
     * APP安全数据解密
     *
     * @param string $data 待解密数据
     * @param string $key 密钥
     * @return mixed
     */
    public function decryptDataByKey($data, $key = null) {
        if ($key === null) {
            $key = $this->secret_key;
        }
        list($blockSize, $keySize) = $this->allowedCiphers[$this->cipher];
        if (strlen($key) !== $keySize) {
            return false;
        }
        $content = base64_decode($data);
        $pos = ord(substr($content, 0, 1)) - 48;
        $encrypt_data = base64_decode(substr($content, 1));
        $iv = substr($key . $key, $pos, $blockSize);
        return openssl_decrypt($encrypt_data, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
    }

    /**
     * 校验签名
     *
     * @param array $data 数据
     * @param string $secret 密钥
     * @param string $signkey 签名键名
     * @param string $noncekey 随机键名
     * @return boolean
     */
    public function validateSignature($data, $secret, $signkey = 'sign', $noncekey = 'nonce') {
        $sign = ArrayHelper::remove($data, $signkey);
        $nonce = ArrayHelper::remove($data, $noncekey);
        ksort($data);
        $signature = hash('sha256', $secret . implode('', $data) . $nonce);
        if ($signature === $sign) {
            return true;
        }
        if (YII_DEBUG) {
            return $signature;
        }
        return false;
    }

    /**
     * APP数据安全加密
     * @param string $data 待加密数据
     * @param string $key 密钥
     * @return mixed
     */
    public function encryptDataByToken($data, $key = null) {
        if ($key === null) {
            return '';
        }
        list($blockSize, $keySize) = $this->allowedCiphers[$this->cipher];
        if (strlen($key) !== $keySize) {
            return false;
        }
        $iv = StringHelper::byteSubstr(strrev($key), 0, $blockSize);
        $encrypt_data = openssl_encrypt($data, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
        return base64_encode($encrypt_data);
    }

    /**
     * APP安全数据解密
     * @param string $data 待解密数据
     * @param string $key 密钥
     * @return mixed
     */
    public function decryptDataByToken($data, $key = null) {
        if ($key === null) {
            $key = null;
        }
        list($blockSize, $keySize) = $this->allowedCiphers[$this->cipher];
        if (strlen($key) !== $keySize) {
            return false;
        }
        $content = base64_decode($data);
        $iv = StringHelper::byteSubstr(strrev($key), 0, $blockSize);
        return openssl_decrypt($content, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
    }

}
